/*
 * Copyright (c) 2012 Banaanvraag
 * MIT Licence
 */

package banaanvraag.tools.btm;

import banaanvraag.tools.arrays.ByteArray;
import banaanvraag.tools.exceptions.InvalidHeaderException;
import banaanvraag.tools.interfaces.Byteable;
import banaanvraag.tools.lists.ByteArrayList;
import java.util.Arrays;

/**
 * Standard BTM v1
 * @author Luc
 */
public class BTM implements Byteable {
    
    /**
     * A BTM class should only support its own version.
     */
    private static final byte version = 1;
    
    /**
     * The general header of a BTM that will always be the first six bytes, independently of the version.
     */
    private static final byte[] superHeader = new byte[]
    {(byte)0x86, (byte)'B', (byte)'T', (byte)'M', version, (byte)0x1A};
    
    private int sizeX, sizeY;
    private Gridbit[] gridbits;
    
    /**
     * Create a Binary Tile Map
     * @param sX The width of the map
     * @param sY The height of the map
     */
    public BTM(int sX, int sY) {
        sizeX = sX;
        sizeY = sY;
        gridbits = new Gridbit[sX * sY];
    }
    
    /**
     * Get the width of the map
     * @return The width of the map, set when the BTM was constructed
     */
    public int getSizeX() {
        return sizeX;
    }
    
    /**
     * Get the height of the map
     * @return The height op the map, set when the BTM was constructed
     */
    public int getSizeY() {
        return sizeY;
    }
    
    /**
     * Place a gridbit on the x and y.
     * The location is calculated as follows:
     * <code>int location = x * sizeX + y</code>
     * @param x The x-coordinate of the gridbit
     * @param y The y-coordinate of the gridbit
     * @param bit The Gridbit (type-meta pair) to be set
     * @throws ArrayIndexOutOfBoundsException
     */
    public void put(int x, int y, Gridbit bit) {
        int location = x * sizeX + y;
        gridbits[location] = bit;
    }
    
    /**
     * Place a gridbit on the x and y, but directly accessing the array.
     * Extra useful in cases where linear access is required.
     * @param location
     * @param bit 
     */
    public void put(int location, Gridbit bit) {
        gridbits[location] = bit;
    }
    
    /**
     * Gets a specific Gridbit from the x and y coordinates.
     * Location is calculated by:
     * <code>int location = x * sizeX + y</code>
     * @param x The x-coordinate of the gridbit
     * @param y The y-coordinate of the gridbit
     * @return The Gridbit pointed at by the x and y
     */
    public Gridbit get(int x, int y) {
        int location = x * sizeX + y;
        return gridbits[location];
    }
    
    /**
     * Gets a specific Gridbit from the map, using direct array access.
     * Especially useful for lnear access.
     * @param location The Location, or array index of the Gridbit
     * @return The Gridbit pointed at by the location
     */
    public Gridbit get(int location) {
        return gridbits[location];
    }
    
    /**
     * Converts this BTM into a byte array.
     * Useful for saving to file and network transmission.
     * @return The byte array containing the whole BTM in binary format
     */
    @Override
    public byte[] toByteArray() {
        ByteArrayList ret = new ByteArrayList();
        
        // Add the header (14 bytes)
        ret.addArray(superHeader);
        ret.addArray(ByteArray.toArray(sizeX));
        ret.addArray(ByteArray.toArray(sizeY));
        
        // Add the gridbits
        int i = 0;
        while(i < sizeX * sizeY) {
            ret.addArray(ByteArray.toArray(gridbits[i].getType()));
            i++;
        }
        i = 0;
        while(i < sizeX * sizeY) {
            ret.addArray(ByteArray.toArray(gridbits[i].getMeta()));
            i++;
        }
        
        return ret.toByteArray();
    }
    
    @Override
    public void fromByteArray(byte[] array) {
        try {
            BTM t = BTM.FromByteArray(array);
            sizeX = t.getSizeX();
            sizeY = t.getSizeY();
            int j, i = 0;
            while(i < sizeX) {
                j = 0;
                while(j < sizeY) {
                    put(i, j, t.get(i, j));
                    j++;
                }
                i++;
            }
        }
        catch(InvalidHeaderException e) {
            sizeX = 1;
            sizeY = 1;
        }
    }
    
    /**
     * Reads a byte array and creates a BTM off of it.
     * This function is static, so it is impossible to do this with an existing
     * BTM, because of initialisation limits.
     * @param array The byte array containing the BTM in binary form
     * @return The BTM that was in the byte array
     * @throws InvalidHeaderException When the header of the BTM is not according to the header that corresponds with this version of the library
     */
    public static BTM FromByteArray(byte[] array) throws InvalidHeaderException {
        ByteArrayList inf = new ByteArrayList();
        inf.addArray(array);
        
        // Check if the header is correct
        byte[] pSH = ByteArray.limitArray(array, 6);
        if(!Arrays.equals(pSH, superHeader)) {
            throw new InvalidHeaderException();
        }
        
        // Determine the size of the map
        byte[] xA = new byte[4];
        xA[0] = inf.get(6);
        xA[1] = inf.get(7);
        xA[2] = inf.get(8);
        xA[3] = inf.get(9);
        
        int sX = ByteArray.toInt(xA);
        
        byte[] yA = new byte[4];
        yA[0] = inf.get(10);
        yA[1] = inf.get(11);
        yA[2] = inf.get(12);
        yA[3] = inf.get(13);
        
        int sY = ByteArray.toInt(yA);
        
        BTM btm = new BTM(sX, sY);
        
        // We'll add the gridbits here
        // First, the type values, which are first in the file.
        int i = 14;
        byte[] tA;
        int t;
        while(i < (4 * sX * sY) + 14) {
            tA = new byte[4];
            tA[0] = inf.get(i);
            tA[1] = inf.get(i + 1);
            tA[2] = inf.get(i + 2);
            tA[3] = inf.get(i + 3);
            
            t = ByteArray.toInt(tA);
            
            btm.put((i - 14) / 4, new Gridbit(t, 0));
            
            i += 4;
        }
        
        // We continue with the meta values, which are stored in the second half of the file.
        byte[] mA;
        int m;
        while(i < (8 * sX * sY) + 14) {
            mA = new byte[4];
            mA[0] = inf.get(i);
            mA[1] = inf.get(i + 1);
            mA[2] = inf.get(i + 2);
            mA[3] = inf.get(i + 3);
            
            m = ByteArray.toInt(mA);
            
            btm.get(((i - 14) - 4 * sX * sY) / 4).setMeta(m);
            
            i += 4;
        }
        
        return btm;
    }
}
